﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using STP;

namespace Simulator
{
	public partial class SheetArea : Control, Bridge.ICallbackHandler
	{
		// We need to lock the _bridges and _wires collections whenever:
		//  - we write to them (Add/Remove/Clear)
		//  - we read from them in the non-main thread.
		// Because we only write to them in the main (interface) thread, we don't need to lock them while reading them from that same main thread.

		State _state;
		List<Bridge> _bridges;
		Bridge _selectedBridge;
		List<Wire> _wires;
		int _selectedPortIndex;
		int _selectedVlanNumber;
		Point _mouseClickLocation;

		internal PropertyGrid _bridgeOrPortPropertyGrid;
		internal PropertyGrid _treePropertyGrid;
		internal Logger _logHandler;
		HintControl _hintControl;
		System.Windows.Forms.Timer _timerComputeMacOperational;

		const string HintNewSheetRightClick = "Right-click on empty area to create new bridges";
		const string HintFirstBridgeMove = "Move bridges around by dragging them with the left mouse button";
		const string HintFirstBridgeDrawWires = "To draw wires between ports, click on an unconnected port and drag the mouse to another unconnected port.";
		const string HintFirstBridgeStart = "Right-click a bridge to power it up or down";
		const string HintMovePorts = "Move ports around the edge of the bridge by dragging them with the left mouse button";

		public SheetArea ()
		{
			InitializeComponent ();

			SetStyle (ControlStyles.OptimizedDoubleBuffer, true);
			SetStyle (ControlStyles.UserPaint, true);
			SetStyle (ControlStyles.AllPaintingInWmPaint, true);
			SetStyle (ControlStyles.ResizeRedraw, true);

			_selectedVlanNumber = -1;

			_hintControl = new HintControl ();
			_hintControl.Parent = this;
			_hintControl.ShowHint (HintNewSheetRightClick);
		}

		protected override void OnHandleCreated (EventArgs e)
		{
			base.OnHandleCreated (e);

			if (IsDesignerHosted == false)
			{
				_bridges = new List<Bridge> ();
				_wires = new List<Wire> ();
				_timerComputeMacOperational = new System.Windows.Forms.Timer ();
				_timerComputeMacOperational.Enabled = true;
				_timerComputeMacOperational.Tick += timerComputeMacOperational_Tick;
			}
		}

		protected override void OnHandleDestroyed (EventArgs e)
		{
			if (!IsDesignerHosted)
			{
				_timerComputeMacOperational.Stop ();
				_timerComputeMacOperational.Dispose ();
				
				foreach (var bridge in _bridges)
					bridge.Dispose ();

				_bridges.Clear ();
			}

			base.OnHandleDestroyed (e);
		}

		// This appears to be the only reliable way to test for design time _outside_ the constructor
		[Browsable (false)]
		public bool IsDesignerHosted
		{
			get
			{
				Control ctrl = this;

				while (ctrl != null)
				{
					if ((ctrl.Site != null) && ctrl.Site.DesignMode)
						return true;
					ctrl = ctrl.Parent;
				}
				return false;
			}
		}

		/// <summary>
		/// This function looks at the user interface (wires and bridges) and calculates macOperational for each port.
		/// It doesn't access STP in any way.
		/// </summary>
		private void timerComputeMacOperational_Tick (object sender, EventArgs e)
		{
			uint timestamp = (uint) Environment.TickCount;

			// -------------------------------------------------------
			// First clear MAC_Operational on all ports on unpowered bridges.

			var portsOnPoweredOffBridges = _bridges.Where (b => !b.Powered).SelectMany (b => b.Ports);
			foreach (var port in portsOnPoweredOffBridges)
				port.MAC_Operational = false;

			// -------------------------------------------------------
			// Now clear MAC_Operational on all ports not connected to a wire.

			var connectedPorts = _wires.SelectMany (w => w.Points).OfType<WirePointPort> ().Select (wpp => wpp.Port);
			var unconnectedPorts = _bridges.SelectMany (b => b.Ports).Except (connectedPorts);
			foreach (var port in unconnectedPorts)
				port.MAC_Operational = false;

			// -------------------------------------------------------
			// Now look for wires with exactly one operational port, and clear MAC_Operational for that port.

			foreach (var wire in _wires)
			{
				var operationalPortsOnThisWire = wire.Points
					.OfType<WirePointPort> ()
					.Select (wpp => wpp.Port)
					.Where (p => p.MAC_Operational);

				using (var enumerator = operationalPortsOnThisWire.GetEnumerator ())
				{
					if (enumerator.MoveNext () == false)
					{
						// zero operational ports
						continue;
					}

					var port = enumerator.Current;

					if (enumerator.MoveNext ())
					{
						// more than one operational port
						continue;
					}

					// exactly one operational port. let's disable it.
					port.MAC_Operational = false;
				}
			}

			// -------------------------------------------------------
			// Now look for wires connected to two or more ports belonging to powered bridges, and set MAC_Operational for these ports.

			foreach (var wire in _wires)
			{
				var ports = wire.Points
					.OfType<WirePointPort> ()
					.Select (wpp => wpp.Port)
					.Where (p => p.Bridge.Powered)
					.ToArray ();

				if (ports.Length == 0)
				{
					// do nothing
				}
				else if (ports.Length == 1)
				{
					// Only one port belonging to a powered bridge. It's supposed to be non-operational, cause we did this above.
					Debug.Assert (ports [0].MAC_Operational == false);
				}
				else if (ports.Length == 2)
				{
					// Let's enable these ports.
					ports [0].MAC_Operational = true;
					ports [1].MAC_Operational = true;
				}
				else
				{
					// multidrop wire
					throw new NotImplementedException ();
				}
			}
		}

		#region Port Info Legend
		struct INFO
		{
			public readonly string str;
			public readonly PortRole role;
			public readonly bool learning;
			public readonly bool forwarding;
			public readonly bool operEdge;

			public INFO (string str, PortRole role, bool learning, bool forwarding, bool operEdge)
			{
				this.str = str;
				this.role = role;
				this.learning = learning;
				this.forwarding = forwarding;
				this.operEdge = operEdge;
			}
		}

		static readonly INFO [] infos = 
		{
			new INFO ("Disabled",						PortRole.Disabled, false, false, false),

			new INFO ("Designated discarding",			PortRole.Designated, false, false, false),
			new INFO ("Designated learning",			PortRole.Designated, true, false, false),
			new INFO ("Designated forwarding",			PortRole.Designated, true, true, false),
			new INFO ("Designated forwarding operEdge",	PortRole.Designated, true, true, true),

			new INFO ("Root/Master discarding",			PortRole.Root, false, false, false),
			new INFO ("Root/Master learning",			PortRole.Root, true, false, false),
			new INFO ("Root/Master forwarding",			PortRole.Root, true, true, false),

			new INFO ("Alternate discarding",			PortRole.Alternate, false, false, false),
			new INFO ("Alternate learning",				PortRole.Alternate, true, false, false),

			new INFO ("Backup discarding",				PortRole.Backup, false, false, false),
		//	new INFO ("Backup learning",				PORT_ROLE_ALTERNATE, true, false, false),
		};

		void PaintLegend (Graphics g)
		{
			var font = SystemFonts.MessageBoxFont;

			SizeF textSizeFloat = g.MeasureString ("Designated forwarding operEdge", font);
			Size textSize = new Size ((int) Math.Ceiling (textSizeFloat.Width), (int) Math.Ceiling (textSizeFloat.Height));

			int bitmapHeight = Port.ExteriorDrawingWidth;
			int bitmapWidth = Port.ExteriorDrawingHeight;
			int textX = ClientSize.Width - (5 + textSize.Width + 5 + bitmapWidth + 5);
			int bitmapX = ClientSize.Width - (5 + bitmapWidth + 5);
			int rowHeight = 1 + Math.Max (textSize.Height, bitmapHeight);
			int bitmapOffsetY = (rowHeight - bitmapHeight) / 2;
			int y = this.ClientSize.Height - infos.Length * rowHeight;

			for (int i = 0; i < infos.Length; i++)
			{
				INFO info = infos [i];

				g.DrawLine (SystemPens.WindowText, textX, y, ClientSize.Width, y);
				g.DrawString (info.str, font, SystemBrushes.WindowText, textX, y + 1);
				Bitmap portBitmap = Port.GetBitmap (info.role, info.learning, info.forwarding, info.operEdge, RotateFlipType.Rotate270FlipNone);
				g.DrawImage (portBitmap, bitmapX, y + bitmapOffsetY + 1);
				y += rowHeight;
			}
		}
		#endregion

		protected override void OnPaint (PaintEventArgs e)
		{
			base.OnPaint (e);

			if (IsDesignerHosted)
				return;

			PaintLegend (e.Graphics);

			Debug.Assert (_selectedVlanNumber != -1);

			// Draw bridges.
			foreach (var bridge in _bridges)
				bridge.Paint (e.Graphics, bridge == _selectedBridge, _selectedPortIndex, (ushort) _selectedVlanNumber);

			// Turn on antialiasing before drawing the wires
			e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;

			// Draw wires.
			foreach (Wire wire in _wires)
				wire.Paint (e.Graphics, (ushort) _selectedVlanNumber);

			if (_state != null)
				_state.OnPaint (e.Graphics);
		}

		Bridge GetBridgeAtCoords (int x, int y)
		{
			foreach (Bridge bridge in _bridges)
			{
				if ((x >= bridge.X) && (x < bridge.X + bridge.Width)
					&& (y >= bridge.Y) && (y < bridge.Y + bridge.Height))
				{
					return bridge;
				}
			}

			return null;
		}

		int GetInteriorPortAtCoords (Bridge bridge, int x, int y)
		{
			for (uint portIndex = 0; portIndex < bridge.PortCount; portIndex++)
			{
				Port port = bridge.Ports [(int) portIndex];

				RectangleF r = port.GetInteriorRect ();

				if ((x >= r.X) && (y >= r.Y) && (x < r.X + r.Width) && (y < r.Y + r.Height))
					return (int) portIndex;
			}

			return -1;
		}

		Port GetExteriorPortAtCoords (int x, int y, int rectangleSize)
		{
			Debug.Assert ((rectangleSize % 2) == 0);

			foreach (var bridge in _bridges)
			{
				foreach (var port in bridge.Ports)
				{
					Point p = port.ConnectorLocation;

					if ((x >= p.X - rectangleSize / 2) && (x < p.X + rectangleSize / 2)
						&& (y >= p.Y - rectangleSize / 2) && (y < p.Y + rectangleSize / 2))
					{
						return port;
					}
				}
			}

			return null;
		}

		bool IsPortWired (Port port)
		{
			foreach (Wire wire in _wires)
			{
				int indexOfPointConnectedToGivenPort = -1;
				int connectionCount = 0;
				for (int pointIndex = 0; pointIndex < wire.Points.Count; pointIndex++)
				{
					WirePoint p = wire.Points [pointIndex];
					if (p is WirePointPort)
					{
						connectionCount++;

						if ((p as WirePointPort).Port == port)
							indexOfPointConnectedToGivenPort = pointIndex;
					}
				}

				if (indexOfPointConnectedToGivenPort != -1)
					return (connectionCount >= 2);
			}

			return false;
		}

		// TODO: change vlan number to int
		public void SelectBridgeOrPortAndVlan (Bridge bridge, int portIndex, ushort vlanNumber)
		{
			if ((bridge != _selectedBridge) || (portIndex != _selectedPortIndex) || (vlanNumber != _selectedVlanNumber))
			{
				_selectedBridge = bridge;
				_selectedPortIndex = portIndex;
				_selectedVlanNumber = vlanNumber;

				if (_selectedBridge == null)
				{
					// nothing selected
					if (_bridgeOrPortPropertyGrid != null)
						_bridgeOrPortPropertyGrid.SelectedObject = null;

					if (_treePropertyGrid != null)
						_treePropertyGrid.SelectedObject = null;

					if (_logHandler != null)
						_logHandler.Select (null, -1, -1);
				}
				else
				{
					// Bridge or port selected

					// Let's bring the bridge's log on screen.

					_logHandler.Select (_selectedBridge, _selectedPortIndex, -1);

					byte treeIndex = _selectedBridge.Wrapper.GetTreeIndexFromVlanNumber ((ushort) _selectedVlanNumber);

					if (_selectedPortIndex == -1)
					{
						// Bridge selected
						_bridgeOrPortPropertyGrid.SelectedObject = _selectedBridge;
						_treePropertyGrid.SelectedObject = _selectedBridge.Trees [treeIndex];
					}
					else
					{
						// Port selected
						_bridgeOrPortPropertyGrid.SelectedObject = _selectedBridge.Ports [_selectedPortIndex];
						_treePropertyGrid.SelectedObject = _selectedBridge.Ports [_selectedPortIndex].Trees [treeIndex];
					}
				}

				Invalidate ();
			}
		}

		protected override void OnMouseDown (MouseEventArgs e)
		{
			base.OnMouseDown (e);

			this.Select ();

			Debug.Assert (_state == null);

			Bridge clickedBridge = GetBridgeAtCoords (e.X, e.Y);
			if (clickedBridge != null)
			{
				// Clicked inside a bridge

				int clickedPortIndex = GetInteriorPortAtCoords (clickedBridge, e.X, e.Y);

				Debug.Assert (_selectedVlanNumber != -1);

				SelectBridgeOrPortAndVlan (clickedBridge, clickedPortIndex, (ushort) _selectedVlanNumber);
			}
			else
			{
				// Clicked outside any bridge.
				Debug.Assert (_selectedVlanNumber != -1);

				SelectBridgeOrPortAndVlan (null, -1, (ushort) _selectedVlanNumber);
			}

			_state = new StateBeginningDrag (this, e);
		}

		protected override void OnMouseMove (MouseEventArgs e)
		{
			base.OnMouseMove (e);

			if (_state != null)
			{
				State newState;
				while (true)
				{
					_state.OnMouseMove (e, out newState);
					if (newState == null)
						break;

					_state = newState;
				}
			}
		}

		protected override void OnMouseUp (MouseEventArgs e)
		{
			base.OnMouseUp (e);

			if (_state != null)
			{
				_state.OnMouseUp ();
				_state = null;
			}
		}

		protected override void OnMouseClick (MouseEventArgs e)
		{
			base.OnMouseClick (e);

			if (e.Button == MouseButtons.Right)
			{
				_mouseClickLocation = e.Location;

				if (_selectedBridge == null)
				{
					// blank area clicked
					contextMenuStripSheet.Show (this, e.X, e.Y);
				}
				else if (_selectedPortIndex == -1)
				{
					// bridge right-clicked
					toolStripMenuItemPowerUpBridge.Enabled = !_selectedBridge.Powered;
					toolStripMenuItemPowerDownBridge.Enabled = _selectedBridge.Powered;

					contextMenuStripBridge.Show (this, e.X, e.Y);
				}
				else
				{
					// port right-clicked
				}
			}
		}

		private void toolStripMenuItemCreateBridge_Click (object sender, EventArgs e)
		{
			using (var form = new NewBridgeForm (this))
			{
				var dr = form.ShowDialog (this);
				if (dr == DialogResult.OK)
				{
					form.NewBridge.CallbackHandler = this;
					lock (form.NewBridge.WrapperLock)
						form.NewBridge.Wrapper.EnableLogging (true);
					form.NewBridge.Location = _mouseClickLocation;
					lock (_bridges)
						_bridges.Add (form.NewBridge);

					Debug.Assert (_selectedVlanNumber != -1);
					SelectBridgeOrPortAndVlan (form.NewBridge, -1, (ushort) _selectedVlanNumber);

					_hintControl.HideHint (HintNewSheetRightClick);

					_hintControl.ShowHint (HintFirstBridgeDrawWires);
					_hintControl.ShowHint (HintFirstBridgeStart);
					_hintControl.ShowHint (HintFirstBridgeMove);
				}
			}
		}

		#region Bridge.ICallbackHandler
		void Bridge.ICallbackHandler.OnDebugStrOut (Bridge bridge, int portIndex, int treeIndex, string str)
		{
			if (InvokeRequired == false)
			{
				if (_logHandler != null)
					_logHandler.AppendText (bridge, portIndex, treeIndex, str);
			}
			else
			{
				BeginInvoke (new Action (() =>
					{
						if (_logHandler != null)
							_logHandler.AppendText (bridge, portIndex, treeIndex, str);
					}));
			}
		}

		void Bridge.ICallbackHandler.OnForwardingChanged (Bridge bridge, uint portIndex, uint treeIndex, bool enabled)
		{
			BeginInvoke (new Action (() => Invalidate ()));
		}

		void Bridge.ICallbackHandler.OnLearningChanged (Bridge bridge, uint portIndex, uint treeIndex, bool enabled)
		{
			BeginInvoke (new Action (() => Invalidate ()));
		}

		void Bridge.ICallbackHandler.OnBpduTransmit (Bridge bridge, uint portIndex, byte [] bpdu)
		{
			Port sourcePort = bridge.Ports [(int) portIndex];
			Port destPort = null;

			lock (_wires)
			{
				foreach (var wire in _wires)
				{
					if ((wire.Points [0] is WirePointPort) && (wire.Points [wire.Points.Count - 1] is WirePointPort))
					{
						if (sourcePort == (wire.Points [0] as WirePointPort).Port)
						{
							destPort = (wire.Points [wire.Points.Count - 1] as WirePointPort).Port;
							break;
						}
						else if (sourcePort == (wire.Points [wire.Points.Count - 1] as WirePointPort).Port)
						{
							destPort = (wire.Points [0] as WirePointPort).Port;
							break;
						}
					}
				}

				if (destPort != null)
					destPort.Bridge.EnqueueReceivedBpdu (destPort.PortIndex, bpdu, (uint) Environment.TickCount);
				else
				{
					// A bridge is trying to send a BPDU from an unconnected port.
					// This scenario is intentionally allowed, as it may occur also in a real bridge. For example, this happens
					// when a wire is pulled out of a port just as STP tries to send something from that port.
				}
			}
		}
		#endregion

		public PropertyGrid TreePropertyGrid
		{
			get { return _treePropertyGrid; }
			set { _treePropertyGrid = value; }
		}

		public PropertyGrid BridgeOrPortPropertyGrid
		{
			get { return _bridgeOrPortPropertyGrid; }
			set { _bridgeOrPortPropertyGrid = value; }
		}

		public Logger Logger
		{
			get { return _logHandler; }
			set { _logHandler = value; }
		}

		private void toolStripMenuItemPowerUpAll_Click (object sender, EventArgs e)
		{
			uint timestamp = (uint) Environment.TickCount;

			foreach (var bridge in _bridges)
				bridge.Powered = true;

			Invalidate ();
		}

		private void toolStripMenuItemPowerDownAll_Click (object sender, EventArgs e)
		{
			uint timestamp = (uint) Environment.TickCount;

			foreach (var bridge in _bridges)
				bridge.Powered = false;

			Invalidate ();
		}

		private void toolStripMenuItemPowerUpBridge_Click (object sender, EventArgs e)
		{
			_selectedBridge.Powered = true;

			_hintControl.HideHint (HintFirstBridgeStart);

			Invalidate ();
		}

		private void toolStripMenuItemPowerDownBridge_Click (object sender, EventArgs e)
		{
			_selectedBridge.Powered = false;

			Invalidate ();
		}

		protected override void OnKeyDown (KeyEventArgs e)
		{
			base.OnKeyDown (e);

			if ((e.Modifiers == Keys.None) && (e.KeyCode == Keys.Delete))
			{
				if (_selectedBridge != null)
				{
					foreach (var wire in _wires)
					{
						// If the wire is connected to the selected bridge and to exactly one other bridge, then disconnect that other bridge.
						int otherBridgeIndexFirst = -1;
						int otherBridgeIndexSecond = -1;
						bool selectedBridgeConnectedToThisWire = false;
						for (int pointIndex = 0; pointIndex < wire.Points.Count; pointIndex++)
						{
							var point = wire.Points [pointIndex];
							if (point is WirePointPort)
							{
								var bridge = (point as WirePointPort).Port.Bridge;
								if (bridge == _selectedBridge)
								{
									wire.Points [pointIndex] = new WirePointFree (wire, point.Location);
									selectedBridgeConnectedToThisWire = true;
								}
								else if (otherBridgeIndexFirst == -1)
								{
									otherBridgeIndexFirst = pointIndex;
								}
								else if (otherBridgeIndexSecond == -1)
								{
									otherBridgeIndexSecond = pointIndex;
								}
							}
						}

						if (selectedBridgeConnectedToThisWire && (otherBridgeIndexFirst != -1) && (otherBridgeIndexSecond == -1))
						{
							// disconnect "otherBridgeFirst"
							throw new NotImplementedException ();
						}
					}

					//if (_logHandler.SelectedBridge == _selectedBridge)
					_logHandler.Select (null, -1, -1);

					if (_bridgeOrPortPropertyGrid.SelectedObject == _selectedBridge)
					{
						_bridgeOrPortPropertyGrid.SelectedObject = null;
						_treePropertyGrid.SelectedObject = null;
					}
					else if ((_bridgeOrPortPropertyGrid.SelectedObject is Port)
						&& ((_bridgeOrPortPropertyGrid.SelectedObject as Port).Bridge == _selectedBridge))
					{
						_bridgeOrPortPropertyGrid.SelectedObject = null;
						_treePropertyGrid.SelectedObject = null;
					}

					lock (_bridges)
						_bridges.Remove (_selectedBridge);

					_selectedBridge.Dispose ();
					_selectedBridge = null;
					Invalidate ();
				}
			}
		}

		public void SelectVlan (int vlanNumber)
		{
			if ((vlanNumber < 1) || (vlanNumber > 4094))
				throw new ArgumentOutOfRangeException ();

			SelectBridgeOrPortAndVlan (_selectedBridge, _selectedPortIndex, (ushort) vlanNumber);

			Invalidate ();
		}

		private void toolStripMenuItemSuspendAll_Click (object sender, EventArgs e)
		{
			foreach (var bridge in _bridges)
				bridge.SuspendPeriodicProcessing ();
		}

		private void toolStripMenuItemResumeAll_Click (object sender, EventArgs e)
		{
			foreach (var bridge in _bridges)
				bridge.ResumePeriodicProcessing ();
		}
		/*
		void EnableDisablePorts (Port portA, Port portB, uint linkSpeedMegabitsPerSecond, bool fullDuplex, uint timestamp)
		{
			portA.Bridge.SuspendStpProcessing ();
			if (portB.Bridge != portA.Bridge)
				portB.Bridge.SuspendStpProcessing ();

			portA.Bridge.EnqueuePortEnabledDisabled (portA.PortIndex, linkSpeedMegabitsPerSecond, fullDuplex, timestamp);
			portB.Bridge.EnqueuePortEnabledDisabled (portB.PortIndex, linkSpeedMegabitsPerSecond, fullDuplex, timestamp);

			portA.Bridge.ResumeStpProcessing ();
			if (portB.Bridge != portA.Bridge)
				portB.Bridge.ResumeStpProcessing ();
		}
		*/
		private void toolStripMenuItemEditMstConfigId_Click (object sender, EventArgs e)
		{
			if (_selectedBridge.ForceProtocolVersion != StpVersion.MSTP)
			{
				MessageBox.Show (this, "This is available only for bridges running MSTP. Try changing ForceProtocolVersion first.", "Can't edit MstConfigId");
				return;
			}

			using (var form = new MstConfigIdForm (_selectedBridge))
			{
				form.ShowDialog (this);
			}
		}

		private void contextMenuStripSheet_Opening (object sender, CancelEventArgs e)
		{
			bool enabled = _bridges.Count > 0;

			toolStripMenuItemPowerUpAll.Enabled = enabled;
			toolStripMenuItemPowerDownAll.Enabled = enabled;
			toolStripMenuItemSuspendAll.Enabled = enabled;
			toolStripMenuItemResumeAll.Enabled = enabled;
		}

		public void SafeInvalidate ()
		{
			if (this.InvokeRequired)
				this.BeginInvoke (new MethodInvoker (this.Invalidate));
			else
				this.Invalidate ();
		}
	}
}